/*
 * Copyright 2005-2021 by Sebastian Thomschke and contributors.
 * SPDX-License-Identifier: EPL-2.0
 */
package net.sf.oval.ogn;

import net.sf.oval.exception.InvalidConfigurationException;
import net.sf.oval.internal.util.Assert;
import net.sf.oval.internal.util.ReflectionUtils;
import org.apache.commons.jxpath.*;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.jxpath.ri.QName;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.ri.model.beans.*;

import java.lang.reflect.AccessibleObject;
import java.util.Locale;
import java.util.Map;

/**
 * <a href="http://commons.apache.org/jxpath/">JXPath</a> based object graph navigator implementation.
 *
 * @author Sebastian Thomschke
 */
public class ObjectGraphNavigatorJXPathImpl implements ObjectGraphNavigator
{

    protected static final class BeanPointerEx extends BeanPointer
    {
        private static final long serialVersionUID = 1L;

        private final JXPathBeanInfo beanInfo;

        public BeanPointerEx(final NodePointer parent, final QName name, final Object bean, final JXPathBeanInfo beanInfo)
        {
            super(parent, name, bean, beanInfo);
            this.beanInfo = beanInfo;
        }

        public BeanPointerEx(final QName name, final Object bean, final JXPathBeanInfo beanInfo, final Locale locale)
        {
            super(name, bean, beanInfo, locale);
            this.beanInfo = beanInfo;
        }

        @Override
        public boolean equals(final Object obj)
        {
            if (obj == this)
                return true;
            if (obj == null || getClass() != obj.getClass())
                return false;
            if (!super.equals(obj))
                return false;
            final BeanPointerEx other = (BeanPointerEx) obj;
            return beanInfo == other.beanInfo;
        }

        @Override
        public int hashCode()
        {
            int hash = 7;
            hash = 31 * hash + super.hashCode();
            hash = 31 * hash + (beanInfo == null ? 0 : beanInfo.hashCode());
            return hash;
        }

        @Override
        public boolean isValidProperty(final QName name)
        {
            if (!super.isValidProperty(name))
                return false;

            // JXPath's default implementation returns true, even if the given property does not exit
            if (beanInfo.getPropertyDescriptor(name.getName()) == null)
                throw new JXPathNotFoundException("No pointer for xpath: " + toString() + "/" + name);

            return true;
        }
    }

    protected static final class BeanPointerFactoryEx extends BeanPointerFactory
    {
        @Override
        public NodePointer createNodePointer(final NodePointer parent, final QName name, final Object bean)
        {
            if (bean == null)
                return new NullPointer(parent, name);

            final JXPathBeanInfo bi = JXPathIntrospector.getBeanInfo(bean.getClass());
            return new BeanPointerEx(parent, name, bean, bi);
        }

        @Override
        public NodePointer createNodePointer(final QName name, final Object bean, final Locale locale)
        {
            final JXPathBeanInfo bi = JXPathIntrospector.getBeanInfo(bean.getClass());
            return new BeanPointerEx(name, bean, bi, locale);
        }

        @Override
        public int getOrder()
        {
            return BeanPointerFactory.BEAN_POINTER_FACTORY_ORDER - 1;
        }
    }

    static {
        /*
         * JXPath currently does not distinguish between invalid object graph paths, e.g. by referencing a non-existing property on a Java Bean,
         * and incomplete object graph paths because of null-values.
         * In both cases a JXPathNotFoundException is thrown if JXPathContext.lenient is <code>false</code>, and in both cases a NullPropertyPointer
         * is returned if JXPathContext.lenient is <code>true</code>.
         *
         * Therefore we install a patched BeanPointerFactory that checks the existence of properties and throws a JXPathNotFoundException if it does
         * not exist, no matter to which setting JXPathContext.lenient is set.
         */
        JXPathContextReferenceImpl.addNodePointerFactory(new BeanPointerFactoryEx());
    }

    @Override
    public ObjectGraphNavigationResult navigateTo(final Object root, final String xpath) throws InvalidConfigurationException
    {
        Assert.argumentNotNull("root", root);
        Assert.argumentNotNull("xpath", xpath);

        try {
            final JXPathContext ctx = JXPathContext.newContext(root);
            ctx.setLenient(true); // do not throw an exception if object graph is incomplete, e.g. contains null-values

            Pointer pointer = ctx.getPointer(xpath);

            // no match found or invalid xpath
            if (pointer instanceof NullPropertyPointer || pointer instanceof NullPointer)
                return null;

            if (pointer instanceof BeanPointer) {
                final Pointer parent = ((BeanPointer) pointer).getImmediateParentPointer();
                if (parent instanceof PropertyPointer) {
                    pointer = parent;
                }
            }

            if (pointer instanceof PropertyPointer) {
                final PropertyPointer pp = (PropertyPointer) pointer;
                final Class<?> beanClass = pp.getBean().getClass();
                AccessibleObject accessor = ReflectionUtils.getField(beanClass, pp.getPropertyName());
                if (accessor == null) {
                    accessor = ReflectionUtils.getGetter(beanClass, pp.getPropertyName());
                    if (accessor == null && Map.class.isAssignableFrom(beanClass)) {
                        accessor = ReflectionUtils.getMethodRecursive(beanClass, "get", Object.class);
                    }
                }
                return new ObjectGraphNavigationResult(root, xpath, pp.getBean(), accessor, pointer.getValue());
            }

            throw new InvalidConfigurationException("Don't know how to handle pointer [" + pointer + "] of type [" + pointer.getClass().getName() + "] for xpath ["
                    + xpath + "]");
        } catch (final JXPathNotFoundException ex) {
            // thrown if the xpath is invalid
            throw new InvalidConfigurationException(ex);
        }
    }
}
